package calculator.rpn.engine.impl;

import calculator.rpn.engine.Calculator;
import calculator.rpn.engine.OperatorAction;
import calculator.rpn.engine.Operators;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.lang3.tuple.Pair;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Stack;
import java.util.function.Supplier;

public class RPNCalculator implements Calculator {
    private Stack<NumberHolder> backupStack = new Stack<>();

    private Stack<BigDecimal> stateStack = new Stack<>();

    private static class NumberHolder {
       final int argCount;
       final BigDecimal value;

        NumberHolder(String value) {
            this(0, new BigDecimal(value));
        }

       NumberHolder(int argCount, BigDecimal value){
           this.argCount = argCount;
           this.value = value;
       }

       boolean isCalculated(){
            return this.argCount > 0;
       }
    }

    public RPNCalculator() {
        Operators.registerActionOperator("undo", this::undo);
        Operators.registerActionOperator("clear", this::clear);
    }

    @Override
    public void append(String input) {
        if(NumberUtils.isCreatable(input)){
            stateStack.push(new BigDecimal(input));
            backupStack.push(new NumberHolder(input));
        } else if(Operators.isCalculatingOperator(input)){
            Pair<Integer, Supplier<OperatorAction>> settings = Operators.getCalculatingOperator(input);
            int size = settings.getKey();
            OperatorAction action = settings.getRight().get();

            if(stateStack.size() < size){
                String msg = "operator <%s> (position: <%d>): insufficient parameters";

                throw new IllegalStateException(String.format(msg, input.trim(), backupStack.size() - 1));
            }

            BigDecimal[] inputs = new BigDecimal[size];
            for(int i = 0 ; i < size; i++){
                inputs[i] = stateStack.pop();
            }

            BigDecimal result = action.calculate(inputs);
            backupStack.push(new NumberHolder(size, result));
            stateStack.push(result);
        } else if(Operators.isActionOperator(input)){
           Operators.getActionOperator(input).run();
        } else {
            throw new IllegalArgumentException("invalid input <" + input + ">: not a number or operator");
        }
    }

    @Override
    public void undo() {
        if(backupStack.size() > 0){
            stateStack.pop();
            NumberHolder lastNumber = backupStack.pop();
            if(lastNumber.isCalculated()){
                for(int i = 0; i< lastNumber.argCount; i++){
                    int index = backupStack.size() - lastNumber.argCount + i;
                    stateStack.push(backupStack.elementAt(index).value);
                }
            }
        }
    }

    @Override
    public void clear() {
        backupStack.clear();
        stateStack.clear();
    }

    @Override
    public BigDecimal calculate() {
        if(backupStack.size() == 0){
            return BigDecimal.ZERO;
        }

        BigDecimal result = stateStack.peek();

        return result;
    }

    @Override
    public void appendSummary(StringBuilder stringBuilder, int scale) {
        stateStack.forEach(n -> {
            stringBuilder.append(new BigDecimal(n.toString()).setScale(scale, RoundingMode.FLOOR).stripTrailingZeros().toPlainString());
            stringBuilder.append(" ");
        });

        if(stateStack.size() > 0){
            stringBuilder.deleteCharAt(stringBuilder.length() - 1);
        }
    }
}
